const OBSWebSocket = require('obs-websocket-js');
const { browser } = require('webextension-polyfill-ts');


/** @type {OBSWebSocket?} */
let obs;
/**
 * `${windowId}/${tabId}` -> OBS source identifier
 * @type {Map<string, {scene_name: string, source_id: number}>}
 */
let mirroring_tabs = new Map();


/**
 * re-initialize the obs instance, if necessary
 */
async function obsws_factory() {
  /* if already connected, test for availability */
  if (typeof obs !== 'undefined') {
    try {
      await obs.send('GetVersion');
      return obs;
    } catch (e) {
      obs = undefined;
    }
  }
  /* connect if not already */
  obs = new OBSWebSocket();
  const config = await browser.storage.local.get(['obswebsocket-passwd']);
  if (typeof config['obswebsocket-passwd'] === 'undefined') {
    browser.notifications.create('OBScure', {
      title: 'OBScure Error', type: 'basic',
      message: 'OBScure is not configured with OBSWebSocket password!'
    });
    throw new Error('no OBSWebSocker password configured');
  }
  await obs.connect({password: config['obswebsocket-passwd']});
  /* NOTE: currently Firefox does not support this */
  if (typeof browser.runtime.onSuspend !== 'undefined')
    browser.runtime.onSuspend.addListener(obs.disconnect);
  return obs;
}


/**
 * Show specified mirroring source
 * may provide an invalid (or untracked) parameter to hide all mirror
 * @param {number} windowId
 * @param {number} tabId
 */
async function show_mirror(windowId, tabId) {
  const key = `${windowId}/${tabId}`;
  for (let [k, {scene_name, source_id}] of mirroring_tabs) {
    await obs.send('SetSceneItemProperties', {
      "scene-name": scene_name, item: {id: source_id},
      visible: key === k
    });
  }
}

/**
 * Initialize mirroing.
 * @param {Tabs.tab} tab
 * @param {{height: number, width: number}} outer
 * @param {{height: number, width: number}} inner
 * @param {{height: number, width: number}} doc
 * @param {number} dpi
 * @param {{x: number, y: number, width: number, height: number,
 *          top: number, bottom: number, left: number, right: number}} el
 */
async function start_mirroring_tab(tab, outer, inner, doc, dpi, el) {
  await obsws_factory();
  const win_title = `${tab.title} — Mozilla Firefox:MozillaWindowClass:firefox.exe`;
  alert(`Adding window_capture ${win_title} to OBS.`);
  /* get crop metrics */
  /* get the naiive crop */
  const raw_crop = {
    top: (outer.height - doc.height) + el.top,
    bottom: doc.height - el.bottom,
    left: el.left,
    right: outer.width - el.left - el.width
  };
  /** adjust the crop, pixel-perfect
   *
   * NOTE: For Windows 10, you may notice that windows have a 1px decoration
   * border, however, if you use the "winrt" capture engine (a.k.a. the
   * "compatibility mode"), it is already cut out. So you may need to adapt to it
   * for a pixel-perfect crop.
   */
  const browser_crop = {
    top: raw_crop.top - 7,
    bottom: raw_crop.bottom - 1,
    left: raw_crop.left,
    right: raw_crop.right - (outer.width - inner.width) - 1
  };
  if (browser_crop.top < 0 || browser_crop.bottom < 0 || browser_crop.left < 0 || browser_crop.right < 0) {
    throw new Error('element not in window');
  }
  /** create scene item
   *
   * NOTE: For there is no obvious browser API to get actual window size __in
   * physical pixels__ , here we use the one returned by a default construction
   * of capture source.
   */
  const scene = await obs.send('GetCurrentScene');
  const scene_name = scene.name;
  /* block visual before source ready */
  // TODO:
  let source = (await obs.send('CreateSource', {
    sourceName: `OBScure::${win_title}::${tab.windowId}/${tab.id}`,
    sourceKind: 'window_capture',
    sceneName: scene_name,
    sourceSettings: {window: win_title},
    setVisible: false
  })).itemId;
  /* scale the crop to match screen DPI */
  const crop = {
    top: Math.ceil(browser_crop.top * dpi),
    bottom: Math.ceil(browser_crop.bottom * dpi),
    left: Math.ceil(browser_crop.left * dpi),
    right: Math.ceil(browser_crop.right * dpi)
  };
  /* move and scale to the correct position */
  const dest_trans_prop = await obs.send('GetSceneItemProperties', {
    "scene-name": scene_name, item: {name: 'OBScure::top'}
  });
  // alert(JSON.stringify(dest_trans_prop));
  const [dest_pos, dest_rect] = [
    dest_trans_prop.position,
    {height: dest_trans_prop.height, width: dest_trans_prop.width}
  ];
  const position = dest_pos;
  let scale;
  if (dest_rect.width / dest_rect.height >= el.width / el.height) {
    /* dest wider than el, scale on height */
    scale = dest_rect.height / (el.height * dpi);
  }
  else {
    /* dest taller than el, scale on width */
    scale = dest_rect.width / (el.width * dpi);
  }
  /* move to the correct rank */
  let items = new Array();
  for (let i of scene.sources) {
    if (i.id === source)
      continue;
    items.push({id: i.id});
    if (i.name === 'OBScure::top')
      items.push({id: source});
  }
  await obs.send('ReorderSceneItems', {
    scene: scene_name, items: items
  });
  /* aplly transform */
  await obs.send('SetSceneItemProperties', {
    "scene-name": scene_name, item: {id: source},
    crop: crop, position: position, scale: {x: scale, y: scale}
  });
  /* extension data structures */
  mirroring_tabs.set(
    `${tab.windowId}/${tab.id}`,
    {scene_name: scene_name, source_id: source}
  );
  show_mirror(tab.windowId, tab.id);
}

/**
 * Stop mirroring and deletes source in OBS. Do nothing if not present.
 * @param {number} tabId
 * @param {number} windowId
 */
async function stop_mirroring_tab(windowId, tabId) {
  const key = `${windowId}/${tabId}`;
  const scene_source = mirroring_tabs.get(key);
  if (typeof scene_source === 'undefined')
    return;
  const {scene_name, source_id} = scene_source;
  await obs.send('DeleteSceneItem', {
    scene: scene_name,
    item: {id: source_id}
  });
  mirroring_tabs.delete(key);
}

/**
 * Stop all mirroring.
 */
async function stop_all_mirroing_tabs() {
  for (let {scene_name, source_id} of mirroring_tabs.values()) {
    await obs.send('DeleteSceneItem', {
      scene: scene_name,
      item: {id: source_id}
    });
  }
  mirroring_tabs.clear();
}


/**
 * Init process
 */
(async () => {
  /* sys env check */
  const rt_info = await browser.runtime.getPlatformInfo();
  alert(JSON.stringify(rt_info));
  if (rt_info.os !== 'win')
    throw new Error('currently only supports windows');

  /* check settings, and set defaults, easier for debugging */
  const config = await browser.storage.local.get(['obswebsocket-passwd']);
  let has_invalid_settings = false;
  if (typeof config['obswebsocket-passwd'] === 'undefined') {
    has_invalid_settings = true;
    await browser.storage.local.set({
      'obswebsocket-passwd': '12345678'
    });
  }
  /* if first visit, show way to option page */
  if (has_invalid_settings)
    browser.runtime.openOptionsPage();

  try {
    await obsws_factory();

    if (true) {
    const version = await obs.send('GetVersion');
    alert(`OBS Studio version: ${version['obs-studio-version']}`);
    alert(`OBSRemote version: ${version.version}`);
    alert(`obs-websocket version: ${version['obs-websocket-version']}`);

    let current_scene = await obs.send('GetCurrentScene');
    alert(`current scene: ${current_scene.name}`);
    alert(`list souces:`);
    for (let i of current_scene.sources) {
      alert(JSON.stringify(i));
      alert(JSON.stringify(await obs.send('GetSourceSettings', {sourceName: i.name})));
      alert(JSON.stringify(await obs.send('GetSceneItemProperties', {
        "scene-name": current_scene.name, item: {id: i.id}
      })));
    }
    }
  } catch (e) {
    // pass
  }

  browser.browserAction.onClicked.addListener(async tab => {
    await browser.tabs.sendMessage(tab.id, {op: 'ToggleMirroringElement'});
  });

  browser.runtime.onMessage.addListener(async (msg, sndr) => {
    alert(`received message: ${JSON.stringify(msg)}`);
    if (msg.op === 'noop')
      return;

    if (msg.op === 'StartMirroringElement') {
      await start_mirroring_tab(
        sndr.tab,
        msg.outer_metrics,
        msg.inner_metrics,
        msg.doc_metrics,
        msg.dpi,
        msg.el_metrics
      );
      return;
    }

    if (msg.op === 'StopMirroringTab') {
      await stop_mirroring_tab(sndr.tab.windowId, sndr.tab.id);
      return;
    }

    throw new Error(`unknown message op ${msg.op}`);
  });

  browser.tabs.onRemoved.addListener(async (tabId, {windowId}) => {
    await stop_mirroring_tab(windowId, tabId)
  });

  browser.tabs.onActivated.addListener(async ({tabId, windowId}) => {
    await show_mirror(windowId, tabId);
  });

})();
